org 0x900

jmp CODE16_START

; 段属性的一些定义，暂时定义一些目前用得到的属性
DA_32       equ    0x4000  ; D/B = 1
DA_DR       equ    0x90    ; 数据，只读
DA_DRW      equ    0x92    ; 数据，可读/写
DA_DRWA     equ    0x93    ; 数据，可读/写，已访问
DA_C        equ    0x98    ; 代码，仅执行
DA_CR       equ    0x9A    ; 代码，可执行，可读
DA_CCO      equ    0x9C    ; 代码，一致性段，仅执行
DA_CCOR     equ    0x9E    ; 代码，一致性段，可执行，可读，已访问
DA_LIMIT_4K equ    0x8000  ; G = 1, 颗粒度：4K

; 段选择符属性定义
SA_RPL0     equ    0     ; RPL = 0
SA_RPL1     equ    1     ; RPL = 1
SA_RPL2     equ    2     ; RPL = 2
SA_RPL3     equ    3     ; RPL = 3
SA_TIG      equ    0     ; TI = 0, GDT
SA_TIL      equ    4     ; TI = 1, LDT

; 段描述符定义
%macro Descriptor 3         ; 有三个参数：段基址、段界限、段属性
    dw  %2 & 0xFFFF         ; 段界限 1  (2 字节)
    dw  %1 & 0xFFFF         ; 段基址 1  (2 字节)
    db  (%1 >> 16) & 0xFF   ; 段基址 2  (1 字节)
    dw  ((%2 >> 8) & 0xF00) | (%3 & 0xF0FF) ; 段属性 1 + 段界限 2 + 段属性 2 (2 字节)
    db  (%1 >> 24) & 0xFF   ; 段基址 3 (1 字节)
%endmacro                   ; 共 8 个字节


; 页目录表与页起始地址
PAGE_DIR_BASE0    equ    0x100000
PAGE_TAB_BASE0    equ    0x101000
PAGE_DIR_BASE1    equ    0x600000
PAGE_TAB_BASE1    equ    0x601000

; 页表相关属性
PG_P             equ    1b      ; 页存在属性位
PG_RW_R          equ    00b     ; R/W 属性位值, 读/执行
PG_RW_W          equ    10b     ; R/W 属性位值, 读/写/执行
PG_US_S          equ    000b    ; U/S 属性位值, 系统级
PG_US_U          equ    100b    ; U/S 属性位值, 用户级

; 全局描述符表定义
;                             段基址           段界限                   段属性
GDT_BASE       : Descriptor     0,              0,                      0
CODE32_DESC    : Descriptor     0,              CODE32_SEG_LEN - 1,     DA_C + DA_32
VIDEO_DESC     : Descriptor     0xB8000,        0xBFFFF - 0xB8000,      DA_DRWA + DA_32
DATA_DESC      : Descriptor     0,              DATA_SEG_LEN - 1,       DA_DR + DA_32
STACK32_DESC   : Descriptor     0,              TOP_OF_STACK32,         DA_DRW + DA_32
PAGE_DIR_DESC0 : Descriptor     PAGE_DIR_BASE0, 4095,                   DA_DRW + DA_32
PAGE_TAB_DESC0 : Descriptor     PAGE_TAB_BASE0, 1023,                   DA_DRW + DA_32 + DA_LIMIT_4K
PAGE_DIR_DESC1 : Descriptor     PAGE_DIR_BASE1, 4095,                   DA_DRW + DA_32
PAGE_TAB_DESC1 : Descriptor     PAGE_TAB_BASE1, 1023,                   DA_DRW + DA_32 + DA_LIMIT_4K
; ...

GDT_LEN         equ   $ - GDT_BASE  ; GDT 长度 = 当前地址 - GDT_BASE 地址

GDT_PTR :
    dw   GDT_LEN - 1
    dd   GDT_BASE

; 段选择符定义，RPL = 0; TI = 0
CODE32_SELECTOR    equ   (0x0001 << 3) + SA_RPL0 + SA_TIG
VIDEO_SELECTOR     equ   (0x0002 << 3) + SA_RPL0 + SA_TIG
DATA_SELECTOR      equ   (0x0003 << 3) + SA_RPL0 + SA_TIG
STACK32_SELECTOR   equ   (0x0004 << 3) + SA_RPL0 + SA_TIG
PAGE_DIR0_SELECTOR equ   (0x0005 << 3) + SA_RPL0 + SA_TIG
PAGE_TAB0_SELECTOR equ   (0x0006 << 3) + SA_RPL0 + SA_TIG
PAGE_DIR1_SELECTOR equ   (0x0007 << 3) + SA_RPL0 + SA_TIG
PAGE_TAB1_SELECTOR equ   (0x0008 << 3) + SA_RPL0 + SA_TIG


msg db "Loader..."	; 使用 db 定义数据
msgLen equ $-msg	; 字符串 msg 长度


[bits 16]
CODE16_START :
    xor ax, ax		; xor 指令与 and or 指令类似，两个操作数的每一对对应位都应用如下操作原则：
					; 如果两个位的值相同（同为 0 或同为 1），则结果位等于 0；否则结果位等于 1
					; xor ax, ax 等于 ax = 0
	mov ss, ax		; 栈段 ss = 0
	mov ds, ax		; 数据段 ds = 0
	mov es, ax		; 附加数据段 es = 0 
	mov sp, 0x900	; 设置栈，跟 0x7c00 一样，0x900 地址以下也有一段安全可用内存

    ; 设置光标位置 (dl, dh)
	mov ah, 0x02    ; AH 功能号 = 0x02，设置光标
	mov dh, 0x01    ; 设置光标行号
	mov dl, 0x00    ; 设置光标列号
	mov bh, 0x00    ; 页码
	int 0x10

    ; 打印 msg
	mov ax, msg
	mov cx, msgLen
	call print

    ; 初始化段描述符中的段基址
    mov esi, CODE32_START
    mov edi, CODE32_DESC
    call InitDescItem

    mov esi, DATA_SEGMENT
    mov edi, DATA_DESC
    call InitDescItem

    mov esi, STACK32_SEGMENT
    mov edi, STACK32_DESC
    call InitDescItem
    
    ; 打开 A20 地址线
    in al, 0x92 
    or al, 0000_0010B 
    out 0x92, al

    ; 加载描述符表
    ;mov eax, 0
    ;mov ax, ds
    ;shl eax, 4
    ;add eax, GDT_BASE
    ;mov dword [GDT_PTR + 2], eax
    lgdt [GDT_PTR]

    ; 通知 CPU 进入保护模式，即将 CR0 寄存器的 PE（bit0） 位置 1
    mov eax, cr0
    or eax, 0x01 
    mov cr0, eax

    ; 刷新流水线
    ; 跳转到 32 位代码段继续执行
    jmp dword CODE32_SELECTOR:0


; 参数： esi --> 代码段标签
; 参数： edi --> 段描述符标签
InitDescItem:
    push eax

    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, esi
    mov word [edi + 2], ax
    shr eax, 16
    mov byte [edi + 4], al
    mov byte [edi + 7], ah

    pop eax
    ret

; 打印字符串
; ax : 输入参数，字符串地址
; cx : 输入参数，字符串长度
print:
	; 入栈
	push ax
	push bp
	push bx

	mov bp, ax      ; 保存字符串地址
	mov ax, 0x1301  ; 子功能号 0x13 是写字符串
	mov bx, 000fh   ; 页号位 0，黑底白字
	int 0x10  

	; 出栈
	pop bx
	pop bp
	pop ax

	ret 


[bits 32]
CODE32_START :
    ; 设置栈寄存器
    mov ax, STACK32_SELECTOR
    mov ss, ax
    mov eax, TOP_OF_STACK32
    mov esp, eax

    ; 设置显存段
	mov ax, VIDEO_SELECTOR
    mov gs, ax   
    
    ; 设置数据段
    mov ax, DATA_SELECTOR
    mov ds, ax

    mov eax, 0
    mov es, eax

    mov ebp, msg2Offset
    mov bl, 0x0F        ; 打印属性，黑底白字
    ; 坐标 (0, 2)
    mov dl, 0x00
    mov dh, 0x02
    call print_str_32

    mov eax, PAGE_DIR0_SELECTOR
    mov ebx, PAGE_TAB0_SELECTOR
    mov ecx, PAGE_TAB_BASE0
    call Init_Page_Table

    mov eax, PAGE_DIR1_SELECTOR
    mov ebx, PAGE_TAB1_SELECTOR
    mov ecx, PAGE_TAB_BASE1
    call Init_Page_Table

    mov eax, PAGE_DIR_BASE0
    call Switch_Page_Table

    mov eax, PAGE_DIR_BASE1
    call Switch_Page_Table

    jmp $


; 使用显存方式打印字符串
; ds:ebp --> 打印的数据起始地址(相对于段基址的偏移地址)
; bl     --> 打印属性
; dx     --> 打印起始坐标 (dl, dh)
print_str_32:
    push ebp
    push eax
    push edi
    push cx
    push dx

  ; 循环  
  s:
    mov cl, [ds:ebp]    ; 取地址 ds:ebp 中的数据存到 cl 寄存器
    cmp cl, 0           ; 比较 cl 是否为 0 
    je end              ; 若为 0 ，就结束打印

    ; 根据坐标 (dl, dh) 计算出偏移量，存入 eax 中
    ; (80 * dh + dl)*2 ; 每行最多显示 80 个字符
    mov eax, 80
    mul dh
    add al, dl
    shl eax, 1          ; eax = eax*2

    mov edi, eax        ; edi ：显存中的偏移量
    mov ah, bl          ; 显示属性
    mov al, cl          ; 要打印的字符
    mov [gs:edi], ax    ; 显示数据放入显存
    inc ebp             ; 自增
    inc dl              ; 自增
    jmp s               ; 循环

; 打印结束
end:
    pop dx
    pop cx
    pop edi
    pop eax
    pop ebp
    
    ret

; 功能：创建页目录表及页表
; 传入参数：
;   eax --> 页目录选择子
;   ebx --> 页表选择子 
;   ecx --> 页表基地址
Init_Page_Table:
    push es
    push eax  ; [esp + 12]
    push ebx  ; [esp + 8]
    push ecx  ; [esp + 4]
    push edi  ; [esp], esp 栈顶指针，始终指向最后一次入栈处，此处 [esp] = edi

; 创建页目录
    mov es, eax         ; eax ==> 页目录选择子
    mov edi, 0          ; es:edi ==> 页目录选择子:0
    mov ecx, 1024       ; 循环 1024 次，页目录共 1024 项
    mov eax, [esp + 4]  ; 
    or eax, PG_P | PG_US_U | PG_RW_W ; eax = 第一个子页表的地址和属性
    cld                 ; edi 自增
.creat_pde:             ; 创建页目录项
    stosd               ; 把 eax 中的值写入 [es:edi] 指向的内存中
    add eax, 4096       ; eax = eax + 4K
    loop .creat_pde
    
    ; 程序执行到这里说明页目录表已经创建完成
    
    ; 创建页表
    mov ax, [esp + 8]   ; 页表选择子
    mov es, eax
    mov edi, 0          ; es:edi ==> 页表选择子:0
    mov ecx, 1024*1024  ;  循环 1024*1024 次，共 1024 个页表，每个页表有 1024 个页表项
    mov eax, PG_P | PG_US_U | PG_RW_W ; 只有属性，没有基地址，因为第一个页表项指向的就是物理地址 0x0 处
    cld
.creat_pte:             ; 创建页表项
    stosd               ; 把 eax 中的值写入 [es:edi] 指向的内存中
    add eax, 4096       ; eax = eax + 4K
    loop .creat_pte

    pop edi
    pop ecx
    pop ebx
    pop eax
    pop es

    ret

; 功能：切换页表  
; 传入参数：
;   eax --> 页目录基地址
Switch_Page_Table:
    push eax

    ; 寄存器 cr0 的 PG 位清 0，暂时先关闭内存分页功能
    mov eax, cr0
    and eax, 0x7FFFFFFF ; and 按位与
    mov cr0, eax

    ; 将页目录表首地址写入控制寄存器 cr3
    mov eax, [esp]      ; [esp] = Switch_Page_Table 运行前 eax
    mov cr3, eax

    ; 寄存器 cr0 的 PG 位置 1，开启内存分页
    mov eax, cr0
    or  eax, 0x80000000
    mov cr0, eax

    pop eax

    ret

  
CODE32_SEG_LEN equ $-CODE32_START

DATA_SEGMENT:
    msg2 db "Enter protection", 0           ; 以 0 为字符串结束标志
    msg2Offset equ  msg2 - DATA_SEGMENT     ; msg2 在数据段 DATA_SEGMENT 中的偏移量
DATA_SEG_LEN equ    $ - DATA_SEGMENT

STACK32_SEGMENT:
    times 1024 * 4 db 0                     ; 开辟 4K 的内存空间当做栈                  
STACK32_SEG_LEN equ    $ - STACK32_SEGMENT
TOP_OF_STACK32  equ    STACK32_SEG_LEN - 1  ; 栈顶 
